Skip to content

Type Bytes.fromList with an all-literal in-range list as total - #808

Merged
jasisz merged 3 commits into
mainfrom
feat/literal-bytes-discharge
Aug 8, 2026
Merged

Type Bytes.fromList with an all-literal in-range list as total#808
jasisz merged 3 commits into
mainfrom
feat/literal-bytes-discharge

Conversation

@jasisz

@jasisz jasisz commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Bytes.fromList([0, 10, 255]) types as plain Bytes and constructs the value directly — no ?, no match. Any other argument shape keeps Result<Bytes, String> exactly as before: an identifier, a computed list, a computed element, an out-of-range literal ([65, 256]), a negative one, or a magnitude beyond i64. The empty list discharges vacuously.

The gate is derived, not named

Nothing in the rule mentions Bytes, fromList, or 0..=255. src/analysis/literal_refinement.rs builds the table from two recognizers that already existed:

  • analysis::shape::detect_module_patterns supplies the RefinementSmartConstructor pattern (carrier field + validating predicate);
  • the packed layout's element-interval derivation, factored out of packed_sequence.rs as element_interval_from_predicate, supplies the bound.

Sharing the second step is the load-bearing part. A packed carrier writes elements into a raw i8 array with no range check, so "the discharge admits this value" and "the packed layout can store it" must be one predicate — they now are, literally the same function. A user refinement with a different range discharges against that range; a record with no validating constructor never discharges.

The table lives on the SymbolTable: the single place entry items and dependency modules are jointly in hand, and the right coupling — a call that cannot resolve cannot discharge. The typechecker and the HIR resolver read that one table, which is what keeps the checked and unchecked pipelines from forking.

Two design points worth review

Spelling insensitivity is forced, not chosen. flatten_multimodule renames a dependency's fromList to a prefixed bare name and rewrites both the qualified and the in-module call sites to it before the wasm-gc backend re-resolves. After the flatten the two spellings are indistinguishable, so a qualified-only rule would discharge before it and not after. Consequence: stdlib/bytes.av's own verify fromList block migrates; a non-literal Ok case was added so the Result contract keeps its coverage. Two refinements sharing a bare constructor name are fail-closed.

The lowering is a construct site, not a call. RecordCreate is total on all four backends and needs no new IR node; proof_lower::multi_field_record_demotions already treats a construct site whose every value is a literal inside the proven interval as exactly as gated as the smart constructor. The rewrite runs on resolved HIR, so the AST-walking demotion scans still see only the constructor call. The alternative — a new UnwrapOk intrinsic — needs a VM opcode plus Rust and wasm-gc arms and has no workable Lean or Dafny story (Bytes has no Inhabited; ? is unsupported in Dafny).

Proof backends

Lean emits ⟨[0, 10, 255], by …⟩, so the kernel re-establishes the gate's own claim instead of assuming it. allInRange is compiled by well-founded recursion, which decide cannot evaluate through the elaborator, so the ladder gains a final simp [<predicate>] rung whose name is read off the refinement's invariant. Dafny discharges the same fact as a subset-type constraint. tests/fixtures/discharged_bytes_law.av is gated by both at budget 0; the 32-element stress case lives in a Lean-only fixture, because Dafny's default function fuel does not unfold a sequence that long.

Migration is loud

A Result / Option constructor pattern whose subject is neither is now a type error. Without it match Bytes.fromList([1, 2]) would silently walk off the end at runtime. Two diagnostic slugs with repair hints were added (error-prop-non-result, pattern-subject-mismatch).

Self-host

Refused, not silently diverged. The Aver-in-Aver resolver has no refinement recognizer — no type defs, no dependency ASTs, no interval derivation — so a mirror is not the three syntactic predicates the literal-divisor rule needed. aver run --self-host and the self-host replay backend now reject a discharged program with an error naming the call sites; the pre-gate symptom was a bare field access on non-record.

Tests

  • Boundary pins named after the boundary, including every fallback shape (tests/typechecker_spec.rs).
  • wasm-gc differential: a discharged literal keeps the proof-derived packed layout (type-section assertion: exactly one more i8 array than the boxed build) and agrees with the VM; the out-of-interval literal declines on all three legs (tests/wasm_gc_packed_sequence.rs).
  • VM / wasm-gc / generated-Rust agreement with discharged and fallible calls in one program (tests/cross_backend_stress.rs, tests/rust_codegen_differential.rs).
  • Lean + Dafny fixtures above; self-host rejection test.

Notes

  • Coverage counter: 77 → 81, zero regressions. The +4 are projects/k5_fdiv functions that Type Int.div and Int.mod with a nonzero literal divisor as total #806's literal-divisor discharge made certifiable; they only surface now because thirteen workflow_engine units were failing to compile on an unmigrated Result.withDefault(Int.div(prev, 4), 0) left over from that PR. projects/workflow_engine/domain/time.av is migrated here so the corpus builds again. tools/coverage-baseline.json is left untouched — refreshing the headline is a separate call.
  • src/analysis/ already depends on codegen::common and codegen::ModuleInfo, so the new module introduces no new layering direction.

🤖 Generated with Claude Code

jasisz and others added 3 commits August 8, 2026 02:40
A call to a recognized `List<Int>` refinement's smart constructor whose
single argument is a syntactic list of integer literals, each inside the
interval the refinement itself proves, cannot reach the constructor's
error branch, so it types as the refined type and lowers to the carrier
construction on every backend: `Bytes.fromList([0, 10, 255]) : Bytes`, no
`?` and no `match`. Any other argument shape keeps `Result<Bytes, String>`
unchanged — an identifier, a computed list, a computed element, an
out-of-range or negative literal, or a magnitude beyond `i64`.

The gate is derived, never named. A new analysis module builds it from two
existing recognizers: the shape tier's `RefinementSmartConstructor` pattern
supplies the carrier field and the validating predicate, and the packed
layout's element-interval derivation (factored out for reuse) supplies the
bound. Sharing the second step is what makes the rule safe on wasm-gc,
where a packed carrier writes elements into a raw `i8` array with no range
check: "the discharge admits it" and "the packed layout can store it" are
now literally the same function. The table lives on the `SymbolTable`, the
one place entry items and dependency modules are jointly in hand, so the
typechecker and the HIR resolver read one table and cannot fork.

Every callee spelling that denotes the constructor decides alike, because
the wasm-gc backend flattens a dependency's constructor and all of its call
sites — qualified and in-module — into one prefixed bare name before
re-resolving. Lean re-establishes the gate's own claim as a `Subtype`
obligation; since the predicate is compiled by well-founded recursion the
tactic ladder gains a `simp [<predicate>]` rung named from the refinement's
invariant. Dafny discharges the same fact as a subset-type constraint.

A `Result` or `Option` constructor pattern whose subject is neither is now
a type error, so the migration is loud rather than a match that silently
walks off the end. Migrates the in-repo call sites, adds the diagnostic
slugs and repair hints, and refuses discharged programs at the self-host
boundary with an error naming the call sites, since the Aver-in-Aver
resolver carries no refinement recognizer. Also migrates one literal-divisor
leftover in the workflow-engine corpus that was failing thirteen units.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The discharge matched a call site by the callee's spelling, so a module
that declares its own `fromList` over an imported refinement's smart
constructor got two answers at once: the type checker obeyed Aver's
shadowing rule and typed `fromList([1, 2, 3])` as the local function's
return type, while the resolver rewrote the very same call into the
refinement's carrier construction. The VM then printed a record where the
program asked for a length. The two backends disagreed as well — after
the wasm-gc flatten the local and the imported name are no longer spelled
alike, so that backend declined the rewrite the VM performed.

Both consult sites now key on the function identity ordinary name
resolution assigned the call. The checker takes the identity out of the
same lookup that produced the signature it checks the call against, and
the resolver uses the callee it just classified: the discharge and the
normal resolution must agree, or the discharge declines. Two recognized
constructors that collapse onto one identity are fail-closed. The
self-host rejection scan asks the resolver's own callee classifier, so it
reports exactly the calls the rewrite fires on and no others.

Keying on identity exposed an older divergence underneath. The wasm-gc
resolver context carried no current module, so a program that spelled its
own members qualified (`Local.fromList(xs)`) failed to resolve there and
lowered to a trap, while every other backend called it. It now carries
the declared module name like every other resolution site.

Also: say in the resolver what actually makes the synthesised
construction safe — the element interval shared with the packed layout,
not the AST demotion scans, which never see the generated node; add a law
over one body that holds a discharged construction and a fallible call at
once; cover the self-host refusal on the replay path; and drop a
recomputation of the refinement table the symbol table already carries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jasisz
jasisz merged commit 6c09811 into main Aug 8, 2026
35 checks passed
@jasisz
jasisz deleted the feat/literal-bytes-discharge branch August 8, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant